avatar

目录
266 - building concept

266 - Building Concept

https://gemini.google.com/u/2/app/2b1cb1ab39ffda6d

好的,同学!欢迎来到 C++ 的世界!我是你的专属 C++ 老师,拥有丰富的开发经验和教学技巧。别担心,就算你是 C++ 新手,我也会用最清晰、最易懂的方式,一步步带你掌握 C++ 的强大功能。

今天我们要学习的是 C++20 引入的一个非常酷的特性:如何创建你自己的概念 (Concepts)。这会让你编写的模板代码更安全、更清晰、也更容易理解。


1. 标题和摘要

标题: C++ 概念:动手创建你自己的类型约束

摘要: 本节课我们将学习如何在 C++20 中定义自己的概念 (Concept)。继上一课使用标准库 (Standard Library) 提供的概念后,本课将重点讲解两种创建自定义概念的语法:一种基于类型萃取 (Type Trait),另一种使用 requires 子句 (requires clause) 来指定更复杂的语法 (syntax) 要求。我们还将学习如何将自定义概念应用到函数模板上。


2. 详细内容解释

我们上一课学习了如何使用 C++ 标准库里已经为我们准备好的概念。它们很好用,但有时我们需要更特定的约束,这时候就需要创建自己的概念了。别担心,这比听起来要简单!

为什么要创建自己的概念?

想象一下,你在写一个函数模板 (template),比如一个加法函数 add(T a, T b)。你希望这个函数能用于整数、浮点数,但不能用于字符串或者其他不能相加的类型。在 C++20 之前,如果你传入了错误的类型,编译器 (compiler) 可能会报出一大堆难以理解的错误信息。而概念就像是给模板参数 T 定下的一系列“规矩”或“要求”,只有满足这些要求的类型才能被接受。如果传入的类型不满足要求,编译器会给出清晰的错误提示,告诉你“这个类型不满足某某概念的要求”。自定义概念让我们可以精确地定义这些“规矩”。

创建自定义概念的两种主要语法:

语法一:基于类型萃取 (Type Trait)

这是最简单的一种方式,适用于你的要求可以通过一个类型萃取来判断的情况。

  • 语法结构:

    C++

    Code
    1
    2
    3
    4
    5
    6
    7
    8
    template <typename T> // 首先,声明一个模板参数,比如 T
    concept 概念名称 = std::某个类型萃取<T>::value; // 或者使用 C++17 的 _v 简化版
    // concept 关键字表明你正在定义一个概念
    // 概念名称 是你给这个概念起的名字(通常用大驼峰命名法)
    // = 号后面是你定义的要求
    // std::某个类型萃取<T>::value 是一个在编译时求值的布尔表达式。
    // 如果这个表达式为 true,则类型 T 满足该概念;否则不满足。
    // 别忘了最后的 ; 分号!
  • 示例:定义一个 MyIntegral 概念

    假设我们想定义一个概念,要求类型必须是整型 (integral) 的(比如 int, char, long 等,但不包括 float, double)。我们可以使用标准库中的 std::is_integral 类型萃取:

    C++

    Code
    1
    2
    3
    4
    #include <type_traits> // 需要包含 <type_traits> 头文件

    template <typename T>
    concept MyIntegral = std::is_integral_v<T>; // 使用 _v 版本更简洁,它等价于 std::is_integral<T>::value

    这段代码定义了一个名为 MyIntegral 的概念。任何类型 T,只要 std::is_integral_v<T> 在编译时计算结果为 true,那么它就满足 MyIntegral 这个概念。

语法二:使用 requires 子句 (requires clause)

当你需要的约束比较复杂,不能简单地用一个类型萃取来表示时,或者你需要检查某些表达式的语法是否有效时,就需要用到 requires 子句。

  • 语法结构:

    C++

    Code
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <typename T, ...> // 可以有一个或多个模板参数
    concept 概念名称 = requires(参数列表) { // 使用 requires 关键字
    // 在这里列出对类型的语法要求
    表达式1;
    表达式2;
    // ...
    // 每个要求都是一个表达式语句,必须以分号结尾
    // 这些表达式本身并不需要计算出有意义的值,编译器只检查它们的语法是否对给定类型有效
    }; // 别忘了最后的分号!
    • requires 关键字后面可以跟一个可选的参数列表 (参数列表),这里的参数就像是临时创建的、用于检查语法的变量。你可以给它们命名,比如 (T a, T b)
    • 花括号 {} 内部包含了一系列的要求(通常是表达式语句)。编译器会检查对于满足该概念的类型,这些语句在语法上是否都有效。
  • 示例 1:定义一个 Multipliable 概念

    我们想定义一个概念,要求两个该类型的对象能够使用 * 运算符相乘。

    C++

    Code
    1
    2
    3
    4
    5
    template <typename T>
    concept Multipliable = requires(T a, T b) { // a 和 b 是用于语法检查的临时变量名
    a * b; // 检查 a * b 这个表达式的语法是否有效
    // 注意:这里只检查语法,不关心 a * b 的结果是什么,也不关心这个操作是否有意义
    };

    如果类型 T(比如 intdouble)的对象支持 * 运算,那么它就满足 Multipliable 概念。但如果 Tstd::string,因为字符串不能直接相乘,所以它不满足这个概念。

  • 示例 2:定义一个 Incrementable 概念

    我们想定义一个概念,要求某个类型的对象支持自增操作(前缀 ++、后缀 ++)和加法赋值 +=。

    C++

    Code
    1
    2
    3
    4
    5
    6
    template <typename T>
    concept Incrementable = requires(T a) {
    a += 1; // 检查 a += 1 是否是有效语法
    ++a; // 检查 ++a 是否是有效语法
    a++; // 检查 a++ 是否是有效语法
    };

    任何支持这三种操作的类型(比如 int, double, 甚至某些自定义的类)都满足 Incrementable 概念。

重要提醒:概念检查的是语法,不是语义或值!

这一点非常重要!当你在 requires 子句中写 a * b; 时,编译器只检查对于类型 T,写 a * b 这行代码会不会导致编译错误。它并不检查 a * b 的结果是不是你期望的,也不检查这个乘法操作在逻辑上是否有意义。同样,Incrementable 概念只检查 ++a; 等语句能否编译通过,不检查 a 的值到底增加了多少。

如何使用自定义概念?

一旦你定义了自己的概念,就可以像使用标准库概念一样,用它来约束你的模板了。主要有四种语法形式(和我们上一课学的一样):

假设我们有之前定义的 MyIntegral 概念和一个 add 函数模板:

  1. requires 子句放在模板声明之后,函数声明之前:

    C++

    Code
    1
    2
    3
    4
    5
    template <typename T>
    requires MyIntegral<T> // 要求 T 必须满足 MyIntegral 概念
    T add(T a, T b) {
    return a + b;
    }
  2. 直接在模板参数列表中使用概念名:

    C++

    Code
    1
    2
    3
    4
    template <MyIntegral T> // 直接声明 T 是一个满足 MyIntegral 的类型
    T add(T a, T b) {
    return a + b;
    }
  3. 拖尾 requires 子句 (Trailing requires clause):

    C++

    Code
    1
    2
    3
    4
    template <typename T>
    T add(T a, T b) requires MyIntegral<T> { // requires 子句放在函数参数列表之后
    return a + b;
    }
  4. auto 结合使用(用于函数参数):

    C++

    Code
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 对于函数参数,可以直接用 概念名 auto 的形式
    // 注意:返回值类型如果是 auto,也需要约束,或者明确写出类型
    MyIntegral auto add(MyIntegral auto a, MyIntegral auto b) {
    return a + b;
    }
    // 或者写成
    // auto add(MyIntegral auto a, MyIntegral auto b) -> MyIntegral auto { // C++20 返回类型推导也可以用概念
    // return a + b;
    // }
    // 更常见的是明确返回值类型或让其自动推导(如果约束允许)
    // auto add(MyIntegral auto a, MyIntegral auto b) { // 返回类型自动推导
    // return a + b;
    // }

这四种语法在使用上是等价的,你可以选择自己喜欢的风格。它们都能达到同样的效果:约束传递给 add 函数的参数类型 T 必须满足 MyIntegral 概念。如果尝试用不满足概念的类型(比如 double)去调用 add 函数,编译器会给出清晰的错误信息,指出违反了 MyIntegral 概念。


3. 代码示例

下面是一个完整的代码示例,演示了如何定义和使用我们上面讨论的 MyIntegral, Multipliable, 和 Incrementable 概念:

C++

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <iostream>
#include <string>
#include <vector>
#include <concepts> // 包含 <concepts> 头文件以使用概念
#include <type_traits> // 包含 <type_traits> 头文件以使用类型萃取

// --- 概念定义 ---

// 语法一:基于类型萃取
template <typename T>
concept MyIntegral = std::is_integral_v<T>;

// 语法二:使用 requires 子句
template <typename T>
concept Multipliable = requires(T a, T b) {
a * b; // 检查语法 a * b 是否有效
};

template <typename T>
concept Incrementable = requires(T a) {
a += 1;
++a;
a++;
};

// --- 使用概念的函数模板 ---

// 使用 MyIntegral 概念 (语法2:直接在模板参数中使用)
template <MyIntegral T>
T add_integral(T a, T b) {
std::cout << "调用 add_integral: ";
return a + b;
}

// 使用 Multipliable 概念 (语法1:requires 子句在前)
template <typename T>
requires Multipliable<T>
void check_multipliable(T a, T b) {
std::cout << "类型支持乘法操作 (*)。\n";
// 注意:这里只是检查了概念,函数内部不一定真的需要执行 a*b
}

// 使用 Incrementable 概念 (语法4:使用 auto)
void check_incrementable(Incrementable auto val) {
std::cout << "类型支持自增操作 (++a, a++, a+=1)。\n";
// 可以在函数内部安全地使用这些操作
val++;
++val;
val += 1;
std::cout << "自增操作后的值 (示例): " << val << std::endl; // 只是为了演示
}


int main() {
int i1 = 5, i2 = 10;
double d1 = 3.14, d2 = 2.71;
std::string s1 = "Hello", s2 = " World";

// 测试 MyIntegral 概念
std::cout << add_integral(i1, i2) << std::endl; // 输出: 调用 add_integral: 15
// std::cout << add_integral(d1, d2) << std::endl; // 编译错误!double 不满足 MyIntegral
// std::cout << add_integral(s1, s2) << std::endl; // 编译错误!string 不满足 MyIntegral

std::cout << "\n--- 测试 Multipliable 概念 ---\n";
check_multipliable(i1, i2); // 输出: 类型支持乘法操作 (*)。
check_multipliable(d1, d2); // 输出: 类型支持乘法操作 (*)。
// check_multipliable(s1, s2); // 编译错误!string 不支持 * 操作符

std::cout << "\n--- 测试 Incrementable 概念 ---\n";
check_incrementable(i1); // 输出: 类型支持自增操作... 自增操作后的值 (示例): 8
check_incrementable(d1); // 输出: 类型支持自增操作... 自增操作后的值 (示例): 6.14
// check_incrementable(s1); // 编译错误!string 不支持自增操作

// 你也可以对指针用 Incrementable (指针支持 ++ 和 += 整数)
int arr[] = {1, 2, 3};
int* ptr = arr;
std::cout << "\n测试指针的 Incrementable:\n";
check_incrementable(ptr); // 输出: 类型支持自增操作... (指针移动后的地址)

return 0;
}

编译和运行:

你需要一个支持 C++20 的编译器(例如 GCC 10+ 或 Clang 10+)。编译时需要启用 C++20 标准。

例如,使用 g++:

g++ -std=c++20 your_code_file.cpp -o your_executable

然后运行:

./your_executable

你会看到成功的输出,并且如果你取消注释掉那些会导致编译错误的行,编译器会给出清晰的关于概念约束不满足的错误信息。


Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

### **4. QA 闪卡 (QA Flash Cards)**

- Q: C++ 中的“概念 (Concept)”是什么?

A: 概念是 C++20 引入的一种特性,它允许我们为模板参数指定一组明确的要求或约束。只有满足这些约束的类型才能用于实例化该模板。

- Q: 为什么要使用概念?

A: 提高代码清晰度、增强类型安全、在编译时捕获类型错误、并产生更友好的编译器错误信息。

- Q: 定义自定义概念的第一种语法是什么?它基于什么?

A: template <typename T> concept Name = 式子; 其中式子通常是一个类型萃取 (Type Trait) 的编译时布尔结果,如 std::is_integral_v<T>。

- Q: 定义自定义概念的第二种语法是什么?它使用哪个关键字?

A: 使用 requires 子句:template <typename T> concept Name = requires(T a) { /* 语法要求 */; };

- Q: requires 子句中的表达式检查的是什么?是语法还是语义/值?

A: 只检查语法 (syntax)。编译器判断这些表达式对于给定类型是否能够编译通过,不关心表达式的计算结果或逻辑意义。

- Q: 如何将自定义概念 MyConcept 应用到一个函数模板 func 的参数 T 上?(至少说出两种方法)

A: 1. template <MyConcept T> void func(T t); 2. template <typename T> requires MyConcept<T> void func(T t); 3. template <typename T> void func(T t) requires MyConcept<T>; 4. void func(MyConcept auto t);
```

---

## **5. 常见误解与易犯错误 (Common Misunderstandings/Mistakes)**

1. **误解概念检查语义:** 最常见的误解是认为概念会检查操作的逻辑意义。例如,认为 `Multipliable` 概念能防止你用一个矩阵类乘以一个不兼容维度的矩阵类(如果乘法操作符本身语法有效的话)。记住,概念只检查语法。
2. **忘记 `#include <concepts>`:** 使用概念(无论是标准库的还是自定义的)通常需要包含 `<concepts>` 头文件。如果使用类型萃取,还需要 `#include <type_traits>`。
3. **语法错误:**
- 忘记概念定义末尾的分号 `;`。
- `requires` 子句后面忘记参数列表 `(T a)`(即使为空也要写 `()`,但通常需要参数来检查表达式)或忘记花括号 `{}`。
- `requires` 子句花括号内的要求语句忘记以分号 `;` 结尾。
4. **混淆 `requires` 关键字的两种用法:** `requires` 既可以用于定义概念(`concept Name = requires(...) {...};`),也可以用于应用概念到模板上(`template <typename T> requires Concept<T> ...` 或 `... requires Concept<T> { ... }`)。注意区分它们的位置和作用。
5. **过度约束:** 有时候可能会定义过于严格的概念,导致一些原本可以工作的类型被拒绝。要仔细考虑你的模板真正需要的最小语法要求是什么。
6. **写成 `require` 而不是 `requires`:** 这是一个常见的拼写错误。关键字是 `requires`。

---

## **6. 编码练习 (Coding Exercise)**

现在轮到你来实践了!请你定义一个名为 `Printable` 的概念。这个概念需要检查一个类型 `T` 是否满足以下两个条件:

1. 该类型的对象 `t` 可以被输出到 `std::ostream`(例如 `std::cout`)。也就是说,`std::cout << t;` 这条语句在语法上是有效的。
2. 该类型的对象支持大于比较运算符 `>`。也就是说,`t > t;` 这条语句在语法上是有效的。

然后,补全下面的函数模板 `compare_and_print`,使其参数 `T` 必须满足你定义的 `Printable` 概念,并完成函数体内的逻辑。

C++

include

include

include // 需要包含

// TODO: 在这里定义你的 Printable 概念
// template
// concept Printable = requires(???) {
// ??? ; // 检查输出流操作
// ??? ; // 检查大于比较操作
// };

// TODO: 使用 Printable 概念约束模板参数 T
// ??? ??? compare_and_print(??? a, ??? b) {
// std::cout << “比较并打印: \n”;
// std::cout << “第一个值: “ << a << std::endl;
// std::cout << “第二个值: “ << b << std::endl;
// if (a > b) {
// std::cout << “第一个值更大。\n”;
// } else if (b > a) { // 为了更完整,我们也检查 b > a
// std::cout << “第二个值更大。\n”;
// } else {
// std::cout << “两个值相等(根据>判断)。\n”;
// }
// }

struct NonPrintable { // 这个结构体不支持 << 输出
int value;
bool operator>(const NonPrintable& other) const { return value > other.value; }
};

struct NonComparable { // 这个结构体不支持 > 比较
int value;
};
// 让 NonComparable 可打印
std::ostream& operator<<(std::ostream& os, const NonComparable& nc) {
os << nc.value;
return os;
}

int main() {
int x = 10, y = 5;
compare_and_print(x, y); // 应该可以工作

std::string s1 = "apple", s2 = "banana";
compare_and_print(s1, s2); // 应该可以工作 (string 支持 << 和 >)

// NonPrintable np1{1}, np2{2};
// compare_and_print(np1, np2); // TODO: 取消注释这一行,应该导致编译错误 (不满足 Printable)

// NonComparable nc1{3}, nc2{4};
// compare_and_print(nc1, nc2); // TODO: 取消注释这一行,应该导致编译错误 (不满足 Printable)

return 0;

}

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136

请你填充上面代码中 `// TODO:` 标记的部分。完成后,把你的答案发给我。我会帮你检查并给出反馈!加油!


homework:

```c++
#include <concepts>  // 需要包含

#include <iostream>

#include <string>



// TODO: 在这里定义你的 Printable 概念

// template <typename T>

// concept Printable = requires(???) {

//     ??? ; // 检查输出流操作

//     ??? ; // 检查大于比较操作

// };

template <typename T>

concept Printable = requires(T a, T b) {

    std::cout << a;

    std::cout << b;

    a > b;

};



// TODO: 使用 Printable 概念约束模板参数 T

template <Printable T>

auto compare_and_print(T a, T b) {

    std::cout << "比较并打印: \n";

    std::cout << "第一个值: " << a << std::endl;

    std::cout << "第二个值: " << b << std::endl;

    if (a > b) {

        std::cout << "第一个值更大。\n";

    } else if (b > a) {  // 为了更完整,我们也检查 b > a

        std::cout << "第二个值更大。\n";

    } else {

        std::cout << "两个值相等(根据>判断)。\n";

    }

}



struct NonPrintable {  // 这个结构体不支持 << 输出

    int value;

    bool operator>(const NonPrintable& other) const {

        return value > other.value;

    }

};



struct NonComparable {  // 这个结构体不支持 > 比较

    int value;

};

// 让 NonComparable 可打印

std::ostream& operator<<(std::ostream& os, const NonComparable& nc) {

    os << nc.value;

    return os;

}



int main() {

    int x = 10, y = 5;

    compare_and_print(x, y);  // 应该可以工作



    std::string s1 = "apple", s2 = "banana";

    compare_and_print(s1, s2);  // 应该可以工作 (string 支持 << 和 >)



    // NonPrintable np1{1}, np2{2};

    // compare_and_print(np1, np2); // TODO: 取消注释这一行,应该导致编译错误

    // (不满足 Printable)



    // NonComparable nc1{3}, nc2{4};

    // compare_and_print(nc1, nc2); // TODO: 取消注释这一行,应该导致编译错误

    // (不满足 Printable)



    return 0;

}

非常棒!你做得非常好!你提交的代码完全正确地实现了我们要求的功能。


7. 解决方案评价、标准答案及解释

评价:

  • 评分: ⭐⭐⭐⭐⭐ (5/5 - 非常出色!)
  • 评价:
    • 你准确地定义了 Printable 概念,使用了 requires 子句来检查两个关键的语法要求:流输出操作 (std::cout << a;) 和大于比较操作 (a > b;)。参数列表 (T a, T b) 的使用也很恰当。
    • 你成功地将 Printable 概念应用到了 compare_and_print 函数模板上,使用了简洁的 template <Printable T> 语法。
    • 函数体内的逻辑与概念所保证的约束一致。
    • 代码结构清晰,易于理解。

标准答案与解释:

你的答案本身就是非常标准的答案!这里我再把它贴出来,并加上一些注释,同时展示一个稍微不同的检查流输出的方式(你的方法也是完全正确的):

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <string>
#include <concepts> // 需要包含
#include <ostream> // 包含 <ostream> 以便使用 std::ostream

// --- Printable 概念定义 ---
// 你的定义方式,完全正确!
template <typename T>
concept Printable_V1 = requires(T a, T b) {
std::cout << a; // 检查是否能输出到 std::cout
// std::cout << b; // 检查 b 其实是多余的,因为 a 和 b 类型相同,但无害
a > b; // 检查是否支持 > 运算符
};

// 另一种常见的检查流输出的方式(也正确)
// 这种方式更通用,因为它不依赖于全局的 std::cout,而是检查任何 std::ostream
template <typename T>
concept Printable_V2 = requires(T val, T other_val, std::ostream& os) {
os << val; // 检查是否能输出到任何 ostream 对象
val > other_val; // 检查是否支持 > 运算符
};

// 我们在下面使用你的版本 Printable_V1,因为它简洁有效
using Printable = Printable_V1; // 给你的版本起个别名,方便下面使用


// --- 使用 Printable 概念约束模板参数 T ---
// 使用语法2:直接在模板参数列表中使用概念名
template <Printable T>
// 返回类型用 void 更明确,因为函数没有返回值;用 auto 也可以,会自动推导为 void
void compare_and_print(T a, T b) {
std::cout << "比较并打印: \n";
std::cout << "第一个值: " << a << std::endl; // 安全,因为 Printable 保证了 << 操作有效
std::cout << "第二个值: " << b << std::endl; // 安全
if (a > b) { // 安全,因为 Printable 保证了 > 操作有效
std::cout << "第一个值更大。\n";
} else if (b > a) { // 出于完整性检查 b > a
std::cout << "第二个值更大。\n";
} else {
std::cout << "两个值相等(根据>判断)。\n";
}
std::cout << "------\n"; // 加个分隔符
}

// --- 用于测试的结构体 ---
struct NonPrintable { // 这个结构体不支持 << 输出
int value;
bool operator>(const NonPrintable& other) const { return value > other.value; }
};

struct NonComparable { // 这个结构体不支持 > 比较
int value;
};
// 让 NonComparable 可打印
std::ostream& operator<<(std::ostream& os, const NonComparable& nc) {
os << nc.value;
return os;
}


int main() {
int x = 10, y = 5;
compare_and_print(x, y); // 输出:比较并打印... 第一个值更大。

std::string s1 = "apple", s2 = "banana";
compare_and_print(s1, s2); // 输出:比较并打印... 第二个值更大。

NonPrintable np1{1}, np2{2};
// compare_and_print(np1, np2); // 取消注释后,编译失败!NonPrintable 不满足 Printable (缺少 <<)

NonComparable nc1{3}, nc2{4};
// compare_and_print(nc1, nc2); // 取消注释后,编译失败!NonComparable 不满足 Printable (缺少 >)

return 0;
}

当你取消注释掉最后两个 compare_and_print 调用并尝试编译时,你会得到清晰的编译器错误信息,指出约束 Printable 未满足,这正是概念带来的好处!


8. 建议与扩展问题 (Suggestions & Extended Questions)

你已经掌握了自定义概念的基础!为了进一步深化理解,可以思考和探索以下问题:

  1. 组合概念: 如果你有一个 Addable 概念检查 a + b 是否有效,你如何定义一个新的概念 AddableAndPrintable,要求一个类型必须 同时 满足 AddablePrintable 两个概念?(提示:想想逻辑运算符)
  2. requires 表达式 (Requires Expressions):requires 子句内部,我们不仅可以写简单的表达式语句,还可以使用更复杂的 requires 表达式 来进行更精细的检查。例如,如何检查 a > b 不仅语法有效,而且其返回结果必须能够转换为 bool 类型?

    C++

    Code
    1
    2
    3
    4
    template <typename T>
    concept StrictComparable = requires(T a, T b) {
    { a > b } -> std::convertible_to<bool>; // 注意这种语法 { expression } -> TypeConstraint;
    };

    你能尝试修改 Printable 概念,使其包含这个更严格的比较检查吗?

  3. 标准库概念探索: C++ 标准库在 <concepts> 头文件中提供了许多有用的预定义概念,比如 std::totally_ordered (要求类型支持 <, >, <=, >=, ==, !=),std::equality_comparable (要求 ==, !=),std::movable, std::copyable, std::invocable 等。花点时间看看这些标准概念的定义和用途,它们能让你的代码更规范。我们的 Printable 概念和 std::totally_ordered 以及流输出有什么关系?
  4. 类模板的概念约束: 我们今天主要将概念用在了函数模板上。你能尝试定义一个类模板,并使用概念来约束它的模板参数吗?例如,一个只能存储 Printable 类型的 Pair 类。

这些问题可以帮助你更好地理解概念的强大功能和灵活性。继续探索,你会发现概念是现代 C++ 中一个非常有用的工具!

如果你对这些扩展问题有任何想法或者尝试了新的代码,随时都可以分享给我!学习 C++ 的旅程,我们一起前进!


评论